
<!DOCTYPE html><html lang="zh-CN">

<head>
  <meta charset="utf-8">
  <meta name="hexo-theme" content="https://github.com/xaoxuu/hexo-theme-stellar/tree/1.29.1" theme-name="Stellar" theme-version="1.29.1">
  
  <meta name="generator" content="Hexo 7.1.1">
  <meta http-equiv='x-dns-prefetch-control' content='on' />
  
  <meta name="renderer" content="webkit">
  <meta name="force-rendering" content="webkit">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1">
  <meta name="HandheldFriendly" content="True" >
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
  <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000">
  <meta name="theme-color" content="#f9fafb">
  
  <title>分布式事务详解 - Sentry's Blog</title>

  
    <meta name="description" content="分布式事务">
<meta property="og:type" content="article">
<meta property="og:title" content="分布式事务详解">
<meta property="og:url" content="https://coclong.gitee.io/2023/11/09/study/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1%E8%AF%A6%E8%A7%A3/index.html">
<meta property="og:site_name" content="Sentry&#39;s Blog">
<meta property="og:description" content="分布式事务">
<meta property="og:locale" content="zh_CN">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550121462.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550122374.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550123521.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550125339.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550125655.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550126299.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550126621.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550126967.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550127318.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550127688.png">
<meta property="og:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550128058.png">
<meta property="og:image" content="https://bu.dusays.com/2022/10/24/63567d3e07da3.png">
<meta property="article:published_time" content="2023-11-08T16:20:03.000Z">
<meta property="article:modified_time" content="2024-03-10T11:50:36.102Z">
<meta property="article:author" content="sentry">
<meta property="article:tag" content="转载">
<meta property="article:tag" content="分布式">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://coclong.gitee.io/images/study/java/xxf-1679550121462.png">
  
  
  
  <meta name="keywords" content="转载,分布式">

  <!-- feed -->
  

  <link rel="stylesheet" href="/css/main.css?v=1.29.1">

  
    <link rel="shortcut icon" href="https://foruda.gitee.com/avatar/1677090168613487741/7674882_coclong_1619494197.png!avatar200">
  

  

  
</head>
<body>

<div class="l_body s:aa content tech" id="start" layout="post" ><aside class="l_left"><div class="leftbar-container">


<header class="header"><div class="logo-wrap"><a class="avatar" href="/about/"><div class="bg" style="opacity:0;background-image:url(https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/avatar/round/rainbow64@3x.webp);"></div><img no-lazy class="avatar" src="https://q1.qlogo.cn/g?b=qq&nk=2930811148&s=100" onerror="javascript:this.classList.add('error');this.src='https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/image/2659360.svg';"></a><a class="title" href="/"><div class="main" ff="title">Sentry's Blog</div><div class="sub normal cap">被人记住的感觉真好啊</div><div class="sub hover cap" style="opacity:0">孤独伴我同行</div></a></div></header>

<div class="nav-area">
<div class="search-wrapper" id="search-wrapper"><form class="search-form"><a class="search-button" onclick="document.getElementById(&quot;search-input&quot;).focus();"><svg t="1705074644177" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1560" width="200" height="200"><path d="M1008.839137 935.96571L792.364903 719.491476a56.783488 56.783488 0 0 0-80.152866 0 358.53545 358.53545 0 1 1 100.857314-335.166073 362.840335 362.840335 0 0 1-3.689902 170.145468 51.248635 51.248635 0 1 0 99.217358 26.444296 462.057693 462.057693 0 1 0-158.255785 242.303546l185.930047 185.725053a51.248635 51.248635 0 0 0 72.568068 0 51.248635 51.248635 0 0 0 0-72.978056z" p-id="1561"></path><path d="M616.479587 615.969233a50.428657 50.428657 0 0 0-61.498362-5.534852 174.655348 174.655348 0 0 1-177.525271 3.484907 49.403684 49.403684 0 0 0-58.833433 6.76482l-3.074918 2.869923a49.403684 49.403684 0 0 0 8.609771 78.10292 277.767601 277.767601 0 0 0 286.992355-5.739847 49.403684 49.403684 0 0 0 8.404776-76.667958z" p-id="1562"></path></svg></a><input type="text" class="search-input" id="search-input" placeholder="站内搜索"></form><div id="search-result"></div><div class="search-no-result">没有找到内容！</div></div>


<nav class="menu dis-select"><a class="nav-item active" title="文章" href="/" style="color:#1BCDFC"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M5.879 2.879C5 3.757 5 5.172 5 8v8c0 2.828 0 4.243.879 5.121C6.757 22 8.172 22 11 22h2c2.828 0 4.243 0 5.121-.879C19 20.243 19 18.828 19 16V8c0-2.828 0-4.243-.879-5.121C17.243 2 15.828 2 13 2h-2c-2.828 0-4.243 0-5.121.879M8.25 17a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 0 1.5H9a.75.75 0 0 1-.75-.75M9 12.25a.75.75 0 0 0 0 1.5h6a.75.75 0 0 0 0-1.5zM8.25 9A.75.75 0 0 1 9 8.25h6a.75.75 0 0 1 0 1.5H9A.75.75 0 0 1 8.25 9" clip-rule="evenodd"/><path fill="currentColor" d="M5.235 4.058C5 4.941 5 6.177 5 8v8c0 1.823 0 3.058.235 3.942L5 19.924c-.975-.096-1.631-.313-2.121-.803C2 18.243 2 16.828 2 14v-4c0-2.829 0-4.243.879-5.121c.49-.49 1.146-.707 2.121-.803zm13.53 15.884C19 19.058 19 17.822 19 16V8c0-1.823 0-3.059-.235-3.942l.235.018c.975.096 1.631.313 2.121.803C22 5.757 22 7.17 22 9.999v4c0 2.83 0 4.243-.879 5.122c-.49.49-1.146.707-2.121.803z" opacity=".5"/></svg></a><a class="nav-item" title="项目" href="/wiki/" style="color:#3DC550"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M14.25 4.48v3.057c0 .111 0 .27.02.406a.936.936 0 0 0 .445.683a.96.96 0 0 0 .783.072c.13-.04.272-.108.378-.159L17 8.005l1.124.534c.106.05.248.119.378.16a.958.958 0 0 0 .783-.073a.936.936 0 0 0 .444-.683c.021-.136.021-.295.021-.406V3.031c.113-.005.224-.01.332-.013C21.154 2.98 22 3.86 22 4.933v11.21c0 1.112-.906 2.01-2.015 2.08c-.97.06-2.108.179-2.985.41c-1.082.286-1.99 1.068-3.373 1.436c-.626.167-1.324.257-1.627.323V5.174c.32-.079 1.382-.203 1.674-.371c.184-.107.377-.216.576-.323m5.478 8.338a.75.75 0 0 1-.546.91l-4 1a.75.75 0 0 1-.364-1.456l4-1a.75.75 0 0 1 .91.546" clip-rule="evenodd"/><path fill="currentColor" d="M18.25 3.151c-.62.073-1.23.18-1.75.336a8.2 8.2 0 0 0-.75.27v3.182l.75-.356l.008-.005a1.13 1.13 0 0 1 .492-.13c.047 0 .094.004.138.01c.175.029.315.1.354.12l.009.005l.749.356V3.647z"/><path fill="currentColor" d="M12 5.214c-.334-.064-1.057-.161-1.718-.339C8.938 4.515 8.05 3.765 7 3.487c-.887-.234-2.041-.352-3.018-.412C2.886 3.007 2 3.9 2 4.998v11.146c0 1.11.906 2.01 2.015 2.079c.97.06 2.108.179 2.985.41c.486.129 1.216.431 1.873.726c1.005.451 2.052.797 3.127 1.034z" opacity=".5"/><path fill="currentColor" d="M4.273 12.818a.75.75 0 0 1 .91-.545l4 1a.75.75 0 1 1-.365 1.455l-4-1a.75.75 0 0 1-.545-.91m.909-4.545a.75.75 0 1 0-.364 1.455l4 1a.75.75 0 0 0 .364-1.455z"/></svg></a><a class="nav-item" title="便笺" href="/notes/" style="color:#FA6400"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M20 12a8 8 0 1 1-16 0a8 8 0 0 1 16 0" opacity=".5"/><path fill="currentColor" d="M17.712 5.453c1.047-.193 2.006-.259 2.797-.152c.77.103 1.536.393 1.956 1.064c.446.714.312 1.542-.012 2.258c-.33.728-.918 1.499-1.672 2.268c-1.516 1.547-3.836 3.226-6.597 4.697c-2.763 1.472-5.495 2.484-7.694 2.92c-1.095.217-2.098.299-2.923.201c-.8-.095-1.6-.383-2.032-1.075c-.47-.752-.296-1.63.07-2.379c.375-.768 1.032-1.586 1.872-2.403L4 12.416c0 .219.083.71.168 1.146c.045.23.09.444.123.596c-.652.666-1.098 1.263-1.339 1.756c-.277.567-.208.825-.145.925c.072.116.305.305.937.38c.609.073 1.44.018 2.455-.183c2.02-.4 4.613-1.351 7.28-2.772c2.667-1.42 4.85-3.015 6.23-4.423c.694-.707 1.15-1.334 1.377-1.836c.233-.515.167-.75.107-.844c-.07-.112-.289-.294-.883-.374c-.542-.072-1.272-.041-2.163.112L16.87 5.656c.338-.101.658-.17.842-.203"/></svg></a><a class="nav-item" title="更多" href="/more/" style="color:#F44336"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m13.629 20.472l-.542.916c-.483.816-1.69.816-2.174 0l-.542-.916c-.42-.71-.63-1.066-.968-1.262c-.338-.197-.763-.204-1.613-.219c-1.256-.021-2.043-.098-2.703-.372a5 5 0 0 1-2.706-2.706C2 14.995 2 13.83 2 11.5v-1c0-3.273 0-4.91.737-6.112a5 5 0 0 1 1.65-1.651C5.59 2 7.228 2 10.5 2h3c3.273 0 4.91 0 6.113.737a5 5 0 0 1 1.65 1.65C22 5.59 22 7.228 22 10.5v1c0 2.33 0 3.495-.38 4.413a5 5 0 0 1-2.707 2.706c-.66.274-1.447.35-2.703.372c-.85.015-1.275.022-1.613.219c-.338.196-.548.551-.968 1.262" opacity=".5"/><path fill="currentColor" d="M10.99 14.308c-1.327-.978-3.49-2.84-3.49-4.593c0-2.677 2.475-3.677 4.5-1.609c2.025-2.068 4.5-1.068 4.5 1.609c0 1.752-2.163 3.615-3.49 4.593c-.454.335-.681.502-1.01.502c-.329 0-.556-.167-1.01-.502"/></svg></a></nav>
</div>
<div class="widgets">


<widget class="widget-wrapper post-list"><div class="widget-header dis-select"><span class="name">最近更新</span></div><div class="widget-body fs14"><a class="item title" href="/2023/07/16/base/%E5%86%99%E4%BD%9C%E5%A7%BF%E5%8A%BF/"><span class="title">写作姿势</span></a><a class="item title" href="/2023/07/11/base/%E5%B8%B8%E7%94%A8%E8%BD%AF%E4%BB%B6/"><span class="title">常用软件</span></a><a class="item title" href="/2023/07/28/base/%E5%8D%9A%E5%AE%A2%E7%BE%8E%E5%8C%96/"><span class="title">博客美化</span></a><a class="item title" href="/2024/04/27/other/%E7%BD%91%E7%BB%9C%E6%97%A5%E5%BF%97/"><span class="title">一周是一年的2%</span></a><a class="item title" href="/2023/08/09/other/%E5%A4%A7%E5%AD%A6%E4%B8%8A%E5%88%B0%E7%8E%B0%E5%9C%A8%E7%BB%88%E4%BA%8E%E6%98%8E%E7%99%BD%E7%9A%84%E4%BA%8B/"><span class="title">大学上到现在终于明白的事</span></a><a class="item title" href="/2023/08/13/study/Java%E4%BB%A3%E7%A0%81%E7%B2%BE%E7%AE%80%E6%8C%87%E5%8D%97/"><span class="title">Java代码精简指南</span></a><a class="item title" href="/2023/06/18/base/Hexo%E6%90%AD%E5%BB%BA%E5%8D%9A%E5%AE%A2%E4%B9%8B%E8%B7%AF/"><span class="title">Hexo搭建博客之路</span></a><a class="item title" href="/2023/09/07/other/%E6%B8%B8%E9%BE%99%E5%BD%93%E5%BD%92%E6%B5%B7/"><span class="title">游龙当归海，海不迎我自来也</span></a><a class="item title" href="/2023/10/02/other/%E8%80%83%E9%A9%BE%E7%85%A7%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%E5%84%BF/"><span class="title">考驾照的那些事儿</span></a><a class="item title" href="/2024/01/15/other/%E9%80%89%E6%8B%A9/"><span class="title">找工作</span></a></div></widget>
</div>
<footer class="footer dis-select"><div class="social-wrap"><a class="social" href="https://gitee.com/coclong" target="_blank" rel="external nofollow noopener noreferrer"><img class="lazy" src="" data-src="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/08a41b181ce68.svg"/></a><a class="social" href="https://wpa.qq.com/msgrd?v=3&uin=2930811148&site=qq&menu=yes&jumpflag=1" target="_blank" rel="external nofollow noopener noreferrer"><img class="lazy" src="" data-src="/images/footer/qq32.svg"/></a><a class="social" href="/" rel="noopener noreferrer"><img class="lazy" src="" data-src="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/3616429.svg"/></a><a class="social" href="/more" rel="noopener noreferrer"><img class="lazy" src="" data-src="https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/social/942ebbf1a4b91.svg"/></a></div></footer>
</div></aside><div class="l_main" id="main">





<div class="article banner top">
  <div class="content">
    <div class="top bread-nav footnote"><div class="left"><div class="flex-row" id="breadcrumb"><a class="cap breadcrumb" href="/">主页</a>
<span class="sep"></span><a class="cap breadcrumb" href="/">文章</a><span class="sep"></span><a class="cap breadcrumb-link" href="/categories/%E6%8A%80%E8%83%BD/">技能</a></div>
<div class="flex-row" id="post-meta"><span class="text created">发布于：<time datetime="2023-11-08T16:20:03.000Z">2023-11-09</time></span><span class="sep updated"></span><span class="text updated">更新于：<time datetime="2024-03-10T11:50:36.102Z">2024-03-10</time></span></div></div></div>
    
    <div class="bottom only-title">
      
      <div class="text-area">
        <h1 class="text title"><span>分布式事务详解</span></h1>
        
      </div>
    </div>
    
  </div>
  </div><article class="md-text content"><div class="tag-plugin colorful note" color="purple"><div class="body"><p>这是一篇转载文章，感觉挺有帮助的~</p></div></div>

<emp>原文链接</emp>：<a class="tag-plugin colorful hashtag" color="green" target="_blank" rel="noopener" href="https://www.yuque.com/tulingzhouyu/sfx8p0/pdbf97lz1qaglnnk"><svg t="1701408144765" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4228" width="200" height="200"><path d="M426.6 64.8c34.8 5.8 58.4 38.8 52.6 73.6l-19.6 117.6h190.2l23-138.6c5.8-34.8 38.8-58.4 73.6-52.6s58.4 38.8 52.6 73.6l-19.4 117.6H896c35.4 0 64 28.6 64 64s-28.6 64-64 64h-137.8l-42.6 256H832c35.4 0 64 28.6 64 64s-28.6 64-64 64h-137.8l-23 138.6c-5.8 34.8-38.8 58.4-73.6 52.6s-58.4-38.8-52.6-73.6l19.6-117.4h-190.4l-23 138.6c-5.8 34.8-38.8 58.4-73.6 52.6s-58.4-38.8-52.6-73.6l19.4-117.8H128c-35.4 0-64-28.6-64-64s28.6-64 64-64h137.8l42.6-256H192c-35.4 0-64-28.6-64-64s28.6-64 64-64h137.8l23-138.6c5.8-34.8 38.8-58.4 73.6-52.6z m11.6 319.2l-42.6 256h190.2l42.6-256h-190.2z" p-id="4229"></path></svg><span>语雀</span></a>

<hr>
<p>分布式事务：在分布式系统中一次操作需要由多个服务协同完成，这种由不同的服务之间通过网络协同完成的事务称为分布式事务</p>
<h2 id="一、2PC"><a href="#一、2PC" class="headerlink" title="一、2PC"></a>一、2PC</h2><p>2PC，两阶段提交，将事务的提交过程分为<u>资源准备</u>和<u>资源提交</u>两个阶段，并且由事务协调者来协调所有事务参与者，如果准备阶段所有事务参与者都预留资源成功，则进行第二阶段的资源提交，否则事务协调者回滚资源。</p>
<h3 id="第一阶段：准备阶段"><a href="#第一阶段：准备阶段" class="headerlink" title="第一阶段：准备阶段"></a>第一阶段：准备阶段</h3><p>由事务协调者询问通知各个事务参与者，是否准备好了执行事务，具体流程如下：</p>
<ul>
<li>① 协调者向所有参与者发送事务内容，询问是否可以提交事务，并等待答复</li>
<li>② 各参与者执行本地事务操作，将 undo 和 redo 信息记入事务日志中（但不提交事务）</li>
<li>③ 如参与者执行成功，给协调者反馈同意，否则反馈中止，表示事务不可以执行</li>
</ul>
<h3 id="第二阶段：提交阶段"><a href="#第二阶段：提交阶段" class="headerlink" title="第二阶段：提交阶段"></a>第二阶段：提交阶段</h3><p>协调者收到各个参与者的准备消息后，根据反馈情况通知各个参与者 commit 提交或者 rollback 回滚</p>
<blockquote>
<p>（1）事务提交</p>
</blockquote>
<p>当第一阶段所有参与者都反馈同意时，则意味着完成事务，协调者发起正式提交事务的请求，具体流程如下：</p>
<ul>
<li>① 协调者节点向所有参与者节点发出正式提交的 commit 请求。</li>
<li>② 收到协调者的 commit 请求后，参与者正式执行事务提交操作，并释放在整个事务期间内占用的资源。</li>
<li>③ 参与者完成事务提交后，向协调者节点发送 ACK 消息。</li>
<li>④ 协调者节点收到所有参与者节点反馈的 ACK 消息后，完成事务。</li>
</ul>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550121462.png" alt="正常提交时，事务的完整流程图" data-fancybox="true"/></div><div class="image-meta"><span class="image-caption center">正常提交时，事务的完整流程图</span></div></div>

<blockquote>
<p>（2）事务回滚</p>
</blockquote>
<p>如果任意一个参与者节点在第一阶段返回的消息为中止，或者协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时，那么这个事务将会被回滚，具体流程如下：</p>
<ul>
<li>① 协调者向所有参与者发出 rollback 回滚操作的请求</li>
<li>② 参与者利用阶段一写入的 undo 信息执行回滚，并释放在整个事务期间内占用的资源</li>
<li>③ 参与者在完成事务回滚之后，向协调者发送回滚完成的 ACK 消息</li>
<li>④ 协调者收到所有参与者反馈的 ACK 消息后，取消事务</li>
</ul>
<h3 id="2PC的缺点"><a href="#2PC的缺点" class="headerlink" title="2PC的缺点"></a>2PC的缺点</h3><p>二阶段提交确实能够提供原子性的操作，但是二阶段提交还是有几个缺点的：</p>
<p>（1）性能问题：执行过程中，所有参与节点都是事务阻塞性的，当参与者占有公共资源时，其他第三方节点访问公共资源就不得不处于阻塞状态，为了数据的一致性而牺牲了可用性，对性能影响较大，不适合高并发高性能场景</p>
<p>（2）可靠性问题：2PC非常依赖协调者，当协调者发生故障时，尤其是第二阶段，那么所有的参与者就会都处于锁定事务资源的状态中，而无法继续完成事务操作（如果是协调者挂掉，可以重新选举一个协调者，但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题）</p>
<p>（3）数据一致性问题：在阶段二中，当协调者向参与者发送 commit 请求之后，发生了局部网络异常或者在发送 commit 请求过程中协调者发生了故障，这会导致只有一部分参与者接收到 commit 请求从而执行提交操作，但是其他部分未接到 commit 请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。</p>
<p>（4）二阶段无法解决的问题：协调者在发出 commit 消息之后宕机，而唯一接收到这条消息的参与者同时也宕机了，那么即使协调者通过选举协议产生了新的协调者，这条事务的状态也是不确定的，没人知道事务是否已经被提交。</p>
<h2 id="二、3PC"><a href="#二、3PC" class="headerlink" title="二、3PC"></a>二、3PC</h2><div class="tag-plugin colorful note" color="yellow"><div class="title"><strong>3PC，三阶段提交协议，是二阶段提交协议的改进版本，三阶段提交有两个改动点</strong></div><div class="body"><p>（1）在协调者和参与者中都引入超时机制</p><p>（2）在第一阶段和第二阶段中插入一个准备阶段，保证了在最后提交阶段之前各参与节点的状态是一致的。</p></div></div>

<p>所以3PC会分为3个阶段，CanCommit 准备阶段、PreCommit 预提交阶段、DoCommit 提交阶段，处理流程如下：</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550122374.png" data-fancybox="true"/></div></div>

<h3 id="阶段一：CanCommit-准备阶段"><a href="#阶段一：CanCommit-准备阶段" class="headerlink" title="阶段一：CanCommit 准备阶段"></a>阶段一：CanCommit 准备阶段</h3><p>协调者向参与者发送 canCommit 请求，参与者如果可以提交就返回 Yes 响应，否则返回 No 响应，具体流程如下：</p>
<ul>
<li>（1）事务询问：协调者向所有参与者发出包含事务内容的 canCommit 请求，询问是否可以提交事务，并等待所有参与者答复。</li>
<li>（2）响应反馈：参与者收到 canCommit 请求后，如果认为可以执行事务操作，则反馈 yes 并进入预备状态，否则反馈 no。</li>
</ul>
<h3 id="阶段二：PreCommit-阶段"><a href="#阶段二：PreCommit-阶段" class="headerlink" title="阶段二：PreCommit 阶段"></a>阶段二：PreCommit 阶段</h3><p>协调者根据参与者的反应情况来决定是否可以进行事务的 PreCommit 操作。根据响应情况，有以下两种可能：</p>
<blockquote>
<p>（1）执行事务</p>
</blockquote>
<p>假如所有参与者均反馈 yes，协调者预执行事务，具体如下：</p>
<ul>
<li>① 发送预提交请求：协调者向参与者发送 PreCommit 请求，并进入准备阶段</li>
<li>② 事务预提交 ：参与者接收到 PreCommit 请求后，会执行本地事务操作，并将 undo 和 redo 信息记录到事务日志中（但不提交事务）</li>
<li>③ 响应反馈 ：如果参与者成功执行了事务操作，则返回 ACK 响应，同时开始等待最终指令。</li>
</ul>
<blockquote>
<p>（2）中断事务</p>
</blockquote>
<p>假如有任何一个参与者向协调者发送了 No 响应，或者等待超时之后，协调者没有接收到所有参与者的响应，那么就执行事务的中断，流程如下：</p>
<ul>
<li>① 发送中断请求 ：协调者向所有参与者发送 abort 请求。</li>
<li>② 中断事务 ：参与者收到来自协调者的 abort 请求之后（或超时之后，仍未收到协调者的请求），执行事务的中断。</li>
</ul>
<h3 id="阶段三：doCommit-阶段"><a href="#阶段三：doCommit-阶段" class="headerlink" title="阶段三：doCommit 阶段"></a>阶段三：doCommit 阶段</h3><p>该阶段进行真正的事务提交，也可以分为以下两种情况：</p>
<blockquote>
<p>（1）提交事务</p>
</blockquote>
<ul>
<li>① 发送提交请求：协调接收到所有参与者发送的 ACK 响应，那么他将从预提交状态进入到提交状态，并向所有参与者发送 doCommit 请求</li>
<li>② 本地事务提交：参与者接收到 doCommit 请求之后，执行正式的事务提交，并在完成事务提交之后释放所有事务资源</li>
<li>③ 响应反馈：事务提交完之后，向协调者发送 ack 响应。</li>
<li>④ 完成事务：协调者接收到所有参与者的 ack 响应之后，完成事务。</li>
</ul>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550123521.png" data-fancybox="true"/></div></div>

<p>上图的“第五步”是错误的，应该是”执行事务但不提交事务“</p>
<blockquote>
<p>（2）中断事务：任何一个参与者反馈 no，或者等待超时后协调者尚无法收到所有参与者的反馈，即中断事务</p>
</blockquote>
<ul>
<li>① 发送中断请求：如果协调者处于工作状态，向所有参与者发出 abort 请求</li>
<li>② 事务回滚：参与者接收到 abort 请求之后，利用其在阶段二记录的 undo 信息来执行事务的回滚操作，并在完成回滚之后释放所有的事务资源。</li>
<li>③ 反馈结果：参与者完成事务回滚之后，向协调者反馈 ACK 消息</li>
<li>④ 中断事务：协调者接收到参与者反馈的 ACK 消息之后，执行事务的中断。</li>
</ul>
<div class="tag-plugin colorful note" color="orange"><div class="body"><p>进入 doCommit 阶段后，无论协调者出现问题，或者协调者与参与者之间的网络出现问题，都会导致参与者无法接收到协调者发出的  doCommit 请求或 abort 请求。此时，参与者会在等待超时之后，继续执行事务提交。这其实基于概率来决定的，当进入第三阶段时，说明第一阶段收到所有参与者的 CanCommit 响应都是 Yes，意味着大家都同意修改了，并且第二阶段所有的参与者对协调者的 PreCommit 请求也都是同意的。所以，一句话概括就是，当进入第三阶段时，由于网络超时等原因，虽然参与者没有收到 commit 或者 abort 响应，但是他有理由相信：成功提交的几率很大。</p></div></div>

<h3 id="3PC的优缺点"><a href="#3PC的优缺点" class="headerlink" title="3PC的优缺点"></a>3PC的优缺点</h3><p>与2PC相比，3PC降低了阻塞范围，并且在等待超时后，协调者或参与者会中断事务，避免了协调者单点问题，当阶段三中协调者出现问题时，参与者会继续提交事务。</p>
<p>数据不一致问题依然存在，假设参与者收到 preCommit 请求后等待 doCommit 指令时，此时协调者请求中断事务，如果因为网络问题无法与所有参与者正常通信，会导致部分参与者继续提交事务，可能造成数据不一致。</p>
<p>2PC和3PC都无法保证数据绝对的一致性，一般为了预防这种问题，可以添加一个报警，比如监控到事务异常的时候，通过脚本自动补偿差异的信息。</p>
<h2 id="三、TCC"><a href="#三、TCC" class="headerlink" title="三、TCC"></a>三、TCC</h2><h3 id="什么是TCC"><a href="#什么是TCC" class="headerlink" title="什么是TCC"></a>什么是TCC</h3><p>TCC（Try Confirm Cancel）是应用层的两阶段提交，所以对代码的侵入性强，其核心思想是：针对每个操作，都要实现对应的确认和补偿操作，也就是业务逻辑的每个分支都需要实现 try、confirm、cancel 三个操作，第一阶段由业务代码编排来调用 Try 接口进行资源预留，当所有参与者的 Try 接口都成功了，事务协调者提交事务，并调用参与者的 confirm 接口真正提交业务操作，否则调用每个参与者的 cancel 接口回滚事务，并且由于 confirm 或者 cancel 有可能会重试，因此对应的部分需要支持幂等。</p>
<h3 id="TCC的执行流程"><a href="#TCC的执行流程" class="headerlink" title="TCC的执行流程"></a>TCC的执行流程</h3><p>TCC的执行流程可以分为两个阶段，分别如下：</p>
<p>（1）第一阶段：Try，业务系统做检测并预留资源 (加锁，锁住资源)，比如常见的下单，在 try 阶段，并不是真正的减库存，而是把下单的库存给锁定住。</p>
<p>（2）第二阶段：根据第一阶段的结果决定是执行 confirm 还是 cancel</p>
<ul>
<li>Confirm：执行真正的业务（执行业务，释放锁）</li>
<li>Cancle：是对 Try 阶段预留资源的释放（出问题，释放锁）</li>
</ul>
<h3 id="TCC如何保证最终一致性"><a href="#TCC如何保证最终一致性" class="headerlink" title="TCC如何保证最终一致性"></a>TCC如何保证最终一致性</h3><ul>
<li>TCC 事务机制以 Try 为中心的，Confirm 确认操作和 Cancel 取消操作都是围绕 Try 而展开。因此，Try 阶段中的操作，其保障性是最好的，即使失败，仍然有 Cancel 取消操作可以将其执行结果撤销。</li>
<li>Try 阶段执行成功并开始执行 Confirm 阶段时，默认 Confirm 阶段是不会出错的，也就是说只要 Try 成功，Confirm 一定成功（TCC设计之初的定义）</li>
<li>Confirm 与 Cancel 如果失败，由 TCC 框架进行重试补偿</li>
<li>存在极低概率在CC环节彻底失败，则需要定时任务或人工介入</li>
</ul>
<h3 id="TCC的注意事项"><a href="#TCC的注意事项" class="headerlink" title="TCC的注意事项"></a>TCC的注意事项</h3><blockquote>
<p>（1）允许空回滚</p>
</blockquote>
<p>空回滚出现的原因是 Try 超时或者丢包，导致 TCC 分布式事务二阶段的回滚，触发 Cancel 操作，此时事务参与者未收到 Try，但是却收到了 Cancel 请求。</p>
<p>所以 cancel 接口在实现时需要允许空回滚，也就是 Cancel 执行时如果发现没有对应的事务 xid 或主键时，需要返回回滚成功，让事务服务管理器认为已回滚。</p>
<blockquote>
<p>（2）防悬挂控制</p>
</blockquote>
<p>悬挂指的是二阶段的 Cancel 比一阶段的 Try 操作先执行，出现该问题的原因是 Try 由于网络拥堵而超时，导致事务管理器生成回滚，触发 Cancel 操作，但之后拥堵在网络的 Try 操作又被资源管理器收到了，但是 Cancel 比 Try 先到。但按照前面允许空回滚的逻辑，回滚会返回成功，事务管理器认为事务已回滚成功，所以此时应该拒绝执行空回滚之后到来的 Try 操作，否则会产生数据不一致。</p>
<p>因此我们可以在 Cancel 空回滚返回成功之前，先记录该条事务 xid 或业务主键，标识这条记录已经回滚过，Try 接口执行前先检查这条事务 xid 或业务主键是否已经标记为回滚成功，如果是则不执行 Try 的业务操作。</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550125339.png" data-fancybox="true"/></div></div>

<blockquote>
<p>（3）幂等控制</p>
</blockquote>
<p>由于网络原因或者重试操作都有可能导致 Try - Confirm - Cancel 3个操作的重复执行，所以使用 TCC 时需要注意这三个操作的幂等控制，通常我们可以使用事务 xid 或业务主键判重来控制。</p>
<h3 id="TCC方案的优缺点"><a href="#TCC方案的优缺点" class="headerlink" title="TCC方案的优缺点"></a>TCC方案的优缺点</h3><p>（1）TCC 事务机制相比于上面介绍的 XA 事务机制，有以下优点：</p>
<ul>
<li>性能提升：具体业务来实现，控制资源锁的粒度变小，不会锁定整个资源。</li>
<li>数据最终一致性：基于 Confirm 和 Cancel 的幂等性，保证事务最终完成确认或者取消，保证数据的一致性。</li>
<li>可靠性：解决了 XA 协议的协调者单点故障问题，由主业务方发起并控制整个业务活动，业务活动管理器也变成多点，引入集群。</li>
</ul>
<p>（2）缺点：TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现，业务耦合度较高，提高了开发成本。</p>
<h2 id="四、Saga事务"><a href="#四、Saga事务" class="headerlink" title="四、Saga事务"></a>四、Saga事务</h2><h3 id="什么是Saga事务"><a href="#什么是Saga事务" class="headerlink" title="什么是Saga事务"></a>什么是Saga事务</h3><p>Saga 事务核心思想是将长事务拆分为多个本地短事务并依次正常提交，如果所有短事务均执行成功，那么分布式事务提交；如果出现某个参与者执行本地事务失败，则由 Saga 事务协调器协调根据相反顺序调用补偿操作，回滚已提交的参与者，使分布式事务回到最初始的状态。Saga 事务基本协议如下：</p>
<ul>
<li>（1）每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。</li>
<li>（2）每个 Ti 都有对应的幂等补偿动作 Ci，补偿动作用于撤销 Ti 造成的结果。</li>
</ul>
<p>与 TCC 事务补偿机制相比，TCC 有一个预留(Try)动作，相当于先保存一个草稿，然后才提交；Saga 事务没有预留动作，直接提交。</p>
<h3 id="Saga的恢复策略"><a href="#Saga的恢复策略" class="headerlink" title="Saga的恢复策略"></a>Saga的恢复策略</h3><p>对于事务异常，Saga 提供了两种恢复策略，分别如下：</p>
<blockquote>
<p>（1）向后恢复(backward recovery)</p>
</blockquote>
<p>当执行事务失败时，补偿所有已完成的事务，是“一退到底”的方式，这种做法的效果是撤销掉之前所有成功的子事务，使得整个 Saga 的执行结果撤销。如下图：</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550125655.png" data-fancybox="true"/></div></div>

<p>从上图可知事务执行到了支付事务T3，但是失败了，因此事务回滚需要从C3,C2,C1依次进行回滚补偿，对应的执行顺序为：T1,T2,T3,C3,C2,C1。</p>
<blockquote>
<p>（2）向前恢复(forward recovery)</p>
</blockquote>
<p>对于执行不通过的事务，会尝试重试事务，这里有一个假设就是每个子事务最终都会成功，这种方式适用于必须要成功的场景，事务失败了重试，不需要补偿。</p>
<h3 id="Saga事务的实现方式"><a href="#Saga事务的实现方式" class="headerlink" title="Saga事务的实现方式"></a>Saga事务的实现方式</h3><p>Saga 事务有两种不同的实现方式，分别如下：</p>
<blockquote>
<p>（1）命令协调（Order Orchestrator）</p>
</blockquote>
<p>中央协调器（Orchestrator，简称 OSO）以命令&#x2F;回复的方式与每项服务进行通信，全权负责告诉每个参与者该做什么以及什么时候该做什么。整体流程如下图：</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550126299.png" data-fancybox="true"/></div></div>

<ul>
<li>① 事务发起方的主业务逻辑请求 OSO 服务开启订单事务</li>
<li>② OSO 向库存服务请求扣减库存，库存服务回复处理结果。</li>
<li>③ OSO 向订单服务请求创建订单，订单服务回复创建结果。</li>
<li>④ OSO 向支付服务请求支付，支付服务回复处理结果。</li>
<li>⑤ 主业务逻辑接收并处理 OSO 事务处理结果回复。</li>
</ul>
<p>中央协调器 OSO 必须事先知道执行整个事务所需的流程，如果有任何失败，它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚，基于中央协调器协调一切时，回滚要容易得多，因为协调器默认是执行正向流程，回滚时只要执行反向流程即可。</p>
<blockquote>
<p>（2）事件编排（Event Choreographyo）</p>
</blockquote>
<p>命令协调方式基于中央协调器实现，所以有单点风险，但是事件编排方式没有中央协调器。事件编排的实现方式中，每个服务可以产生自己的事件并监听其他服务的事件来决定是否采取行动。</p>
<p>在事件编排方法中，第一个服务执行一个事务，然后发布一个事件，该事件被一个或多个服务进行监听，这些服务再执行本地事务并发布（或不发布）新的事件。当最后一个服务执行本地事务并且不发布任何事件时，意味着分布式事务结束，或者它发布的事件没有被任何 Saga 参与者监听到都意味着事务结束。</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550126621.png" data-fancybox="true"/></div></div>

<ul>
<li>① 事务发起方的主业务逻辑发布开始订单事件。</li>
<li>② 库存服务监听开始订单事件，扣减库存，并发布库存已扣减事件。</li>
<li>③ 订单服务监听库存已扣减事件，创建订单，并发布订单已创建事件。</li>
<li>④ 支付服务监听订单已创建事件，进行支付，并发布订单已支付事件。</li>
<li>⑤ 主业务逻辑监听订单已支付事件并处理。</li>
</ul>
<p>如果事务涉及 2 至 4 个步骤，则非常合适使用事件编排方式，它是实现 Saga 模式的自然方式，它很简单，容易理解，不需要太多的代码来构建。</p>
<h3 id="Saga事务的优缺点"><a href="#Saga事务的优缺点" class="headerlink" title="Saga事务的优缺点"></a>Saga事务的优缺点</h3><blockquote>
<p>（1）命令协调设计的优缺点</p>
</blockquote>
<p>① 优点：</p>
<ul>
<li>服务之间关系简单，避免服务间循环依赖，因为 Saga 协调器会调用 Saga 参与者，但参与者不会调用协调器。</li>
<li>程序开发简单，只需要执行命令&#x2F;回复(其实回复消息也是一种事件消息)，降低参与者的复杂性。</li>
<li>易维护扩展，在添加新步骤时，事务复杂性保持线性，回滚更容易管理，更容易实施和测试。</li>
</ul>
<p>② 缺点：</p>
<ul>
<li>中央协调器处理逻辑容易变得庞大复杂，导致难以维护。</li>
<li>存在协调器单点故障风险。</li>
</ul>
<blockquote>
<p>（2）事件编排设计的优缺点</p>
</blockquote>
<p>① 优点：</p>
<ul>
<li>避免中央协调器单点故障风险。</li>
<li>当涉及的步骤较少服务开发简单，容易实现。</li>
</ul>
<p>② 缺点：</p>
<ul>
<li>服务之间存在循环依赖的风险。</li>
<li>当涉及的步骤较多，服务间关系混乱，难以追踪调测。</li>
</ul>
<p>由于 Saga 模型没有 Prepare 阶段，因此事务间不能保证隔离性。当多个 Saga 事务操作同一资源时，就会产生更新丢失、脏数据读取等问题，这时需要在业务层控制并发，例如：在应用层面加锁，或者应用层面预先冻结资源。</p>
<h2 id="五、本地消息表"><a href="#五、本地消息表" class="headerlink" title="五、本地消息表"></a>五、本地消息表</h2><h3 id="什么是本地消息表"><a href="#什么是本地消息表" class="headerlink" title="什么是本地消息表"></a>什么是本地消息表</h3><p>本地消息表的核心思路就是将分布式事务拆分成本地事务进行处理，在该方案中主要有两种角色：事务主动方和事务被动方。事务主动发起方需要额外新建事务消息表，并在本地事务中完成业务处理和记录事务消息，并轮询事务消息表的数据发送事务消息，事务被动方基于消息中间件消费事务消息表中的事务。</p>
<p>这样可以避免以下两种情况导致的数据不一致性：</p>
<ul>
<li>业务处理成功、事务消息发送失败</li>
<li>业务处理失败、事务消息发送成功</li>
</ul>
<h3 id="本地消息表的执行流程"><a href="#本地消息表的执行流程" class="headerlink" title="本地消息表的执行流程"></a>本地消息表的执行流程</h3><div class="tag-plugin colorful note" child="tabs"><div class="body"><div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550126967.png" data-fancybox="true"/></div></div><p>① 事务主动方在一个本地事务中处理业务和写消息表操作</p><p>② 事务主动方通过消息中间件，通知事务被动方处理事务消息。消息中间件可以基于 Kafka、RocketMQ 消息队列，事务主动方主动写消息到消息队列，事务消费方消费并处理消息队列中的消息。</p><p>③ 事务被动方通过消息中间件，通知事务主动方事务已处理的消息。</p><p>④ 事务主动方接收中间件的消息，更新消息表的状态为已处理。</p></div></div>

<details class="tag-plugin colorful folding" child="tabs" open><summary><span>一些必要的容错处理如下</span></summary><div class="body"><ul><li>当①处理出错，由于还在事务主动方的本地事务中，直接回滚即可</li><li>当②、③处理出错，由于事务主动方本地保存了消息，只需要轮询消息重新通过消息中间件发送，通知事务被动方重新读取消息处理业务即可。</li><li>如果是业务上处理失败，事务被动方可以发消息给事务主动方回滚事务</li><li>如果事务被动方已经消费了消息，事务主动方需要回滚事务的话，需要发消息通知事务主动方进行回滚事务。</li></ul></div></details>

<h3 id="本地消息表的优缺点"><a href="#本地消息表的优缺点" class="headerlink" title="本地消息表的优缺点"></a>本地消息表的优缺点</h3><blockquote>
<p>（1）优点</p>
</blockquote>
<ul>
<li>从应用设计开发的角度实现了消息数据的可靠性，消息数据的可靠性不依赖于消息中间件，弱化了对 MQ 中间件特性的依赖</li>
<li>方案轻量，容易实现</li>
</ul>
<blockquote>
<p>（2）缺点</p>
</blockquote>
<ul>
<li>与具体的业务场景绑定，耦合性强，不可公用</li>
<li>消息数据与业务数据同库，占用业务系统资源</li>
<li>业务系统在使用关系型数据库的情况下，消息服务性能会受到关系型数据库并发性能的局限</li>
</ul>
<h2 id="六、MQ事务消息"><a href="#六、MQ事务消息" class="headerlink" title="六、MQ事务消息"></a>六、MQ事务消息</h2><h3 id="MQ事务消息的执行流程"><a href="#MQ事务消息的执行流程" class="headerlink" title="MQ事务消息的执行流程"></a>MQ事务消息的执行流程</h3><p>基于 MQ 的分布式事务方案本质上是对本地消息表的封装，整体流程与本地消息表一致，唯一不同的就是将本地消息表存在了 MQ 内部，而不是业务数据库中，如下图：</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550127318.png" data-fancybox="true"/></div></div>

<p>由于将本地消息表存在了 MQ 内部，那么 MQ 内部的处理尤为重要，下面主要基于 RocketMQ4.3 之后的版本介绍 MQ 的分布式事务方案</p>
<h3 id="RocketMQ事务消息"><a href="#RocketMQ事务消息" class="headerlink" title="RocketMQ事务消息"></a>RocketMQ事务消息</h3><p>在本地消息表方案中，保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务，而 RocketMQ 的事务消息相对于普通 MQ 提供了 2PC 的提交接口，方案如下：</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550127688.png" data-fancybox="true"/></div></div>

<blockquote>
<p>（1）正常情况</p>
</blockquote>
<p>在事务主动方服务正常，没有发生故障的情况下，发消息流程如下：</p>
<ul>
<li>步骤①：发送方向 MQ Server(MQ服务方) 发送 half 消息</li>
<li>步骤②：MQ Server 将消息持久化成功之后，向发送方 ack 确认消息已经发送成功</li>
<li>步骤③：发送方开始执行本地事务逻辑</li>
<li>步骤④：发送方根据本地事务执行结果向 MQ Server 提交二次确认（commit 或是 rollback）</li>
<li>最终步骤：MQ Server 如果收到的是 commit 操作，则将半消息标记为可投递，MQ 订阅方最终将收到该消息；若收到的是 rollback 操作则删除 half 消息，订阅方将不会收到该消息</li>
</ul>
<blockquote>
<p>（2）异常情况</p>
</blockquote>
<p>在断网或者应用重启等异常情况下，图中的步骤④提交的二次确认超时未到达 MQ Server，此时的处理逻辑如下：</p>
<ul>
<li>步骤⑤：MQ Server 对该消息发起消息回查</li>
<li>步骤⑥：发送方收到消息回查后，需要检查对应消息的本地事务执行的最终结果</li>
<li>步骤⑦：发送方根据检查得到的本地事务的最终状态再次提交二次确认</li>
<li>最终步骤：MQ Server基于 commit&#x2F;rollback 对消息进行投递或者删除</li>
</ul>
<h3 id="MQ事务消息的优缺点"><a href="#MQ事务消息的优缺点" class="headerlink" title="MQ事务消息的优缺点"></a>MQ事务消息的优缺点</h3><p>（1）优点：相比本地消息表方案，MQ 事务方案优点是：</p>
<ul>
<li>消息数据独立存储 ，降低业务系统与消息系统之间的耦合</li>
<li>吞吐量大于使用本地消息表方案</li>
</ul>
<p>（2）缺点：</p>
<ul>
<li>一次消息发送需要两次网络请求(half 消息 + commit&#x2F;rollback 消息) </li>
<li>业务处理服务需要实现消息状态回查接口</li>
</ul>
<h2 id="七、最大努力通知"><a href="#七、最大努力通知" class="headerlink" title="七、最大努力通知"></a>七、最大努力通知</h2><p>最大努力通知也称为定期校对，是对 MQ 事务方案的进一步优化。它在事务主动方增加了消息校对的接口，如果事务被动方没有接收到主动方发送的消息，此时可以调用事务主动方提供的消息校对的接口主动获取</p>
<div class="tag-plugin image"><div class="image-bg"><img class="lazy" src="" data-src="/images/study/java/xxf-1679550128058.png" data-fancybox="true"/></div></div>

<p>在可靠消息事务中，事务主动方需要将消息发送出去，并且让接收方成功接收消息，这种可靠性发送是由事务主动方保证的；但是最大努力通知，事务主动方仅仅是尽最大努力（重试，轮询….）将消息发送给事务接收方，所以存在事务被动方接收不到消息的情况，此时需要事务被动方主动调用事务主动方的消息校对接口查询业务消息并消费，这种通知的可靠性是由事务被动方保证的。</p>
<p>所以最大努力通知适用于业务通知类型，例如微信交易的结果，就是通过最大努力通知方式通知各个商户，既有回调通知，也有交易查询接口。</p>
<h2 id="八、各方案常见使用场景总结"><a href="#八、各方案常见使用场景总结" class="headerlink" title="八、各方案常见使用场景总结"></a>八、各方案常见使用场景总结</h2><ul>
<li>2PC&#x2F;3PC：依赖于数据库，能够很好的提供强一致性和强事务性，但延迟比较高，比较适合传统的单体应用，在同一个方法中存在跨库操作的情况，<mark class="tag-plugin colorful mark" color="red">不适合高并发和高性能要求的场景</mark> 。</li>
<li>TCC：准备阶段业务多，锁粒度小，性能要求高，  只要把 try、confirm、cancel 各阶段的逻辑捋清楚就可以使用 TCC 了， 但是存在业务耦合。</li>
<li>本地消息表&#x2F;MQ 事务：适用于事务中参与方支持操作幂等，对一致性要求不高，业务上能<mark class="tag-plugin colorful mark" color="red">容忍数据不一致</mark> 到一个人工检查周期，事务涉及的参与方、参与环节较少，业务上有对账&#x2F;校验系统兜底。</li>
<li>Saga 事务：由于 Saga 事务不能保证隔离性，需要在业务层控制并发，适合于业务场景事务并发操作同一资源较少的情况。Saga 由于缺少预提交动作，导致补偿动作的实现比较麻烦，例如业务是发送短信，补偿动作则得再发送一次短信说明撤销，用户体验比较差。所以，Saga 事务<mark class="tag-plugin colorful mark" color="red">较适用于补偿动作容易处理的场景</mark></li>
</ul>
<div class="tag-plugin quot"><h2 class="content" id="Seata AT 模式" type="icon"><img class="icon prefix lazy" src="" data-src="https://bu.dusays.com/2022/10/24/63567d3e07da3.png" /><span class="text">Seata AT 模式</span><span class="empty"></span></h2></div>

<p>AT 模式，是 seata 的默认&#x2F;独有模式，也是实际项目中比较常用的一种模式，采用的也是两阶段提交，不过弥补了 XA 模式中资源锁定周期过长的缺点，相对于 XA 来说，性能更好一些，但缺点就是数据不是强一致，因为它的数据会真实的提交到数据库的，而如果后面做分支事务有问题的话，回滚靠的是日志来实现最终一致。</p>
<p>AT 模式需要用到几张数据库表：</p>
<ul>
<li><p>镜像日志表：undo_log</p>
</li>
<li><p>分支事务表：branch_table</p>
</li>
<li><p>全局事务表：global_table</p>
</li>
<li><p>全局锁表：lock_table</p>
</li>
</ul>
<div class="tag-plugin colorful folders" ><details class="folder" index="0"><summary><span>一阶段</span></summary><div class="body"><p>1、解析 SQL：得到 SQL 的类型（UPDATE），表（table），条件（where name &#x3D; ‘xxx’）等相关的信息</p><p>2、查询前镜像：根据解析得到的条件信息，生成查询语句，定位数据，得到前镜像</p><p>3、执行业务 SQL：操作这条记录</p><p>4、查询后镜像：根据前镜像的结果，通过<strong>主键</strong>定位数据，得到后镜像</p><p>5、插入回滚日志：把前后镜像数据以及业务 SQL 相关的信息组成一条回滚日志记录，插入到 UNDO_LOG 表中</p><p>6、提交前，向 TC 注册分支：申请 table 表中，等于该主键值的记录的<strong>全局锁</strong> </p><p>7、本地事务提交：业务数据的更新和前面步骤中生成的 UNDO_LOG 一并提交</p><p>8、将本地事务提交的结果上报给 TC</p></div></details><details class="folder" index="1"><summary><span>二阶段-提交</span></summary><div class="body"><p>1、收到 TC 的分支提交请求，把请求放入一个异步任务的队列中，马上返回提交成功的结果给 TC</p><p>2、异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录</p></div></details><details class="folder" index="2"><summary><span>二阶段-回滚</span></summary><div class="body"><p>收到 TC 的分支回滚请求，开启一个本地事务，执行如下操作：</p><p>1、通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。</p><p>2、数据校验：拿 UNDO LOG 中的后镜像与当前数据进行比较，如果有不同，说明数据被当前全局事务之外的动作做了修改。这种情况，需要根据配置策略来做处理，默认会有重试机制。</p><p>3、根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句</p><p>4、提交本地事务，并把本地事务的执行结果（即分支事务回滚的结果）上报给 TC</p></div></details></div>
<div class="article-footer fs14">
    <section id="references">
      <div class="header"><span>参考资料</span></div>
      <div class="body">
        <ul>
        <li class="post-title">
          <p><a target="_blank" rel="noopener" href="https://www.yuque.com/tulingzhouyu/sfx8p0/pdbf97lz1qaglnnk">七种常见分布式事务详解</a></p>

        </li>
        
        <li class="post-title">
          <p><a target="_blank" rel="noopener" href="https://baijiahao.baidu.com/s?id=1773353152487462965">Seata AT模式使用及底层原理</a></p>

        </li>
        </ul>
      </div>
    </section>
    
    <section id="license">
      <div class="header"><span>许可协议</span></div>
      <div class="body"><p>本文采用 <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">署名-非商业性使用-相同方式共享 4.0 国际</a> 许可协议，转载请注明出处。</p>
</div>
    </section>
    </div>
</article>
<div class="related-wrap" id="read-next"><section class="body"><div class="item" id="prev"><div class="note">较新文章</div><a href="/2024/01/15/other/%E9%80%89%E6%8B%A9/">找工作</a></div><div class="item" id="next"><div class="note">较早文章</div><a href="/2023/11/03/study/23%E7%A7%8D%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/">23种设计模式</a></div></section></div>

<div class="related-wrap" id="related-posts">
    <section class='header'>
      <div class='title cap theme'>您可能感兴趣的文章</div>
    </section>
    <section class='body'>
    <div class="related-posts"><a class="item" href="\2023\08\09\other\大学上到现在终于明白的事\" title="大学上到现在终于明白的事"><span class="title">大学上到现在终于明白的事</span><span class="excerpt">大学上到现在终于明白的事</span></a><a class="item" href="\2023\11\03\study\23种设计模式\" title="23种设计模式"><span class="title">23种设计模式</span><span class="excerpt">设计模式</span></a></div></section></div>




<footer class="page-footer footnote"><hr><div class="sitemap"><div class="sitemap-group"><span class="fs15">博客</span><a href="/">近期</a><a href="/categories/">分类</a><a href="/tags/">标签</a><a href="/archives/">归档</a></div><div class="sitemap-group"><span class="fs15">项目</span><a href="/wiki/">笔记</a><a target="_blank" rel="noopener" href="https://gitee.com/coclong">开源库</a></div><div class="sitemap-group"><span class="fs15">社交</span><a href="/notes/">友链</a><a href="/">留言板</a><a target="_blank" rel="noopener" href="https://space.bilibili.com/415988557">哔哩哔哩</a></div><div class="sitemap-group"><span class="fs15">更多</span><a href="/more">关于本站</a><a target="_blank" rel="noopener" href="https://gitee.com/coclong">Gitee</a><a target="_blank" rel="noopener" href="https://github.com/Godlike-long">GitHub</a></div></div><div class="text"><p>本站由 <a href="/">sentry</a> 使用 <a target="_blank" rel="noopener" href="https://github.com/xaoxuu/hexo-theme-stellar/tree/1.29.1">Stellar 1.29.1</a> 主题创建。<br>本博客所有文章除特别声明外，均采用 <a target="_blank" rel="noopener" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a> 许可协议，转载请注明出处。</p>
</div></footer>
<div class="main-mask" onclick="sidebar.dismiss()"></div></div><aside class="l_right">
<div class="widgets">



<widget class="widget-wrapper toc" id="data-toc" collapse="false"><div class="widget-header dis-select"><span class="name">本文目录</span><a class="cap-action" onclick="sidebar.toggleTOC()" ><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6h11m-11 6h11m-11 6h11M4 6h1v4m-1 0h2m0 8H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg></a></div><div class="widget-body"><ol class="toc"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%80%E3%80%812PC"><span class="toc-text">一、2PC</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%AC%AC%E4%B8%80%E9%98%B6%E6%AE%B5%EF%BC%9A%E5%87%86%E5%A4%87%E9%98%B6%E6%AE%B5"><span class="toc-text">第一阶段：准备阶段</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E7%AC%AC%E4%BA%8C%E9%98%B6%E6%AE%B5%EF%BC%9A%E6%8F%90%E4%BA%A4%E9%98%B6%E6%AE%B5"><span class="toc-text">第二阶段：提交阶段</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#2PC%E7%9A%84%E7%BC%BA%E7%82%B9"><span class="toc-text">2PC的缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BA%8C%E3%80%813PC"><span class="toc-text">二、3PC</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%98%B6%E6%AE%B5%E4%B8%80%EF%BC%9ACanCommit-%E5%87%86%E5%A4%87%E9%98%B6%E6%AE%B5"><span class="toc-text">阶段一：CanCommit 准备阶段</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%98%B6%E6%AE%B5%E4%BA%8C%EF%BC%9APreCommit-%E9%98%B6%E6%AE%B5"><span class="toc-text">阶段二：PreCommit 阶段</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E9%98%B6%E6%AE%B5%E4%B8%89%EF%BC%9AdoCommit-%E9%98%B6%E6%AE%B5"><span class="toc-text">阶段三：doCommit 阶段</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#3PC%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">3PC的优缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%89%E3%80%81TCC"><span class="toc-text">三、TCC</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AFTCC"><span class="toc-text">什么是TCC</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#TCC%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B"><span class="toc-text">TCC的执行流程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#TCC%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%9C%80%E7%BB%88%E4%B8%80%E8%87%B4%E6%80%A7"><span class="toc-text">TCC如何保证最终一致性</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#TCC%E7%9A%84%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9"><span class="toc-text">TCC的注意事项</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#TCC%E6%96%B9%E6%A1%88%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">TCC方案的优缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9B%9B%E3%80%81Saga%E4%BA%8B%E5%8A%A1"><span class="toc-text">四、Saga事务</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AFSaga%E4%BA%8B%E5%8A%A1"><span class="toc-text">什么是Saga事务</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Saga%E7%9A%84%E6%81%A2%E5%A4%8D%E7%AD%96%E7%95%A5"><span class="toc-text">Saga的恢复策略</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Saga%E4%BA%8B%E5%8A%A1%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F"><span class="toc-text">Saga事务的实现方式</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#Saga%E4%BA%8B%E5%8A%A1%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">Saga事务的优缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BA%94%E3%80%81%E6%9C%AC%E5%9C%B0%E6%B6%88%E6%81%AF%E8%A1%A8"><span class="toc-text">五、本地消息表</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%AC%E5%9C%B0%E6%B6%88%E6%81%AF%E8%A1%A8"><span class="toc-text">什么是本地消息表</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%9C%AC%E5%9C%B0%E6%B6%88%E6%81%AF%E8%A1%A8%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B"><span class="toc-text">本地消息表的执行流程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#%E6%9C%AC%E5%9C%B0%E6%B6%88%E6%81%AF%E8%A1%A8%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">本地消息表的优缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%85%AD%E3%80%81MQ%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF"><span class="toc-text">六、MQ事务消息</span></a><ol class="toc-child"><li class="toc-item toc-level-3"><a class="toc-link" href="#MQ%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B"><span class="toc-text">MQ事务消息的执行流程</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#RocketMQ%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF"><span class="toc-text">RocketMQ事务消息</span></a></li><li class="toc-item toc-level-3"><a class="toc-link" href="#MQ%E4%BA%8B%E5%8A%A1%E6%B6%88%E6%81%AF%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">MQ事务消息的优缺点</span></a></li></ol></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%B8%83%E3%80%81%E6%9C%80%E5%A4%A7%E5%8A%AA%E5%8A%9B%E9%80%9A%E7%9F%A5"><span class="toc-text">七、最大努力通知</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%85%AB%E3%80%81%E5%90%84%E6%96%B9%E6%A1%88%E5%B8%B8%E8%A7%81%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E6%80%BB%E7%BB%93"><span class="toc-text">八、各方案常见使用场景总结</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#Seata%20AT%20%E6%A8%A1%E5%BC%8F"><span class="toc-text">Seata AT 模式</span></a></li></ol></div><div class="widget-footer">

<a class="top" onclick="util.scrollTop()"><svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 12c0-4.714 0-7.071 1.464-8.536C4.93 2 7.286 2 12 2c4.714 0 7.071 0 8.535 1.464C22 4.93 22 7.286 22 12c0 4.714 0 7.071-1.465 8.535C19.072 22 16.714 22 12 22s-7.071 0-8.536-1.465C2 19.072 2 16.714 2 12Z"/><path stroke-linecap="round" stroke-linejoin="round" d="m9 15.5l3-3l3 3m-6-4l3-3l3 3"/></g></svg><span>回到顶部</span></a></div></widget>
</div></aside><div class='float-panel blur'>
  <button type='button' style='display:none' class='laptop-only rightbar-toggle mobile' onclick='sidebar.rightbar()'>
    <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6h11m-11 6h11m-11 6h11M4 6h1v4m-1 0h2m0 8H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
  </button>
  <button type='button' style='display:none' class='mobile-only leftbar-toggle mobile' onclick='sidebar.leftbar()'>
    <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 11c0-3.771 0-5.657 1.172-6.828C4.343 3 6.229 3 10 3h4c3.771 0 5.657 0 6.828 1.172C22 5.343 22 7.229 22 11v2c0 3.771 0 5.657-1.172 6.828C19.657 21 17.771 21 14 21h-4c-3.771 0-5.657 0-6.828-1.172C2 18.657 2 16.771 2 13z"/><path id="sep" stroke-linecap="round" d="M5.5 10h6m-5 4h4m4.5 7V3"/></g></svg>
  </button>
</div>
</div><div class="scripts">
<script type="text/javascript">
  const ctx = {
    date_suffix: {
      just: `刚刚`,
      min: `分钟前`,
      hour: `小时前`,
      day: `天前`,
    },
    root : `/`,
  };

  // required plugins (only load if needs)
  if (`local_search`) {
    ctx.search = {};
    ctx.search.service = `local_search`;
    if (ctx.search.service == 'local_search') {
      let service_obj = Object.assign({}, `{"field":"all","path":"/search.json","content":true,"sort":"-date"}`);
      ctx.search[ctx.search.service] = service_obj;
    }
  }
  const def = {
    avatar: `https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/avatar/round/3442075.svg`,
    cover: `https://gcore.jsdelivr.net/gh/cdn-x/placeholder@1.0.12/cover/76b86c0226ffd.svg`,
  };
  const deps = {
    jquery: `https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js`,
    marked: `https://cdn.jsdelivr.net/npm/marked@13.0.1/lib/marked.umd.min.js`
  }
  

</script>

<script type="text/javascript">
  const utils = {
    // 懒加载 css https://github.com/filamentgroup/loadCSS
    css: (href, before, media, attributes) => {
      var doc = window.document;
      var ss = doc.createElement("link");
      var ref;
      if (before) {
        ref = before;
      } else {
        var refs = (doc.body || doc.getElementsByTagName("head")[0]).childNodes;
        ref = refs[refs.length - 1];
      }
      var sheets = doc.styleSheets;
      if (attributes) {
        for (var attributeName in attributes) {
          if (attributes.hasOwnProperty(attributeName)) {
            ss.setAttribute(attributeName, attributes[attributeName]);
          }
        }
      }
      ss.rel = "stylesheet";
      ss.href = href;
      ss.media = "only x";
      function ready(cb) {
        if (doc.body) {
          return cb();
        }
        setTimeout(function () {
          ready(cb);
        });
      }
      ready(function () {
        ref.parentNode.insertBefore(ss, before ? ref : ref.nextSibling);
      });
      var onloadcssdefined = function (cb) {
        var resolvedHref = ss.href;
        var i = sheets.length;
        while (i--) {
          if (sheets[i].href === resolvedHref) {
            return cb();
          }
        }
        setTimeout(function () {
          onloadcssdefined(cb);
        });
      };
      function loadCB() {
        if (ss.addEventListener) {
          ss.removeEventListener("load", loadCB);
        }
        ss.media = media || "all";
      }
      if (ss.addEventListener) {
        ss.addEventListener("load", loadCB);
      }
      ss.onloadcssdefined = onloadcssdefined;
      onloadcssdefined(loadCB);
      return ss;
    },

    js: (src, opt) => new Promise((resolve, reject) => {
      var script = document.createElement('script');
      if (src.startsWith('/')){
        src = ctx.root + src.substring(1);
      }
      script.src = src;
      if (opt) {
        for (let key of Object.keys(opt)) {
          script[key] = opt[key]
        }
      } else {
        // 默认异步，如果需要同步，第二个参数传入 {} 即可
        script.async = true
      }
      script.onerror = reject
      script.onload = script.onreadystatechange = function() {
        const loadState = this.readyState
        if (loadState && loadState !== 'loaded' && loadState !== 'complete') return
        script.onload = script.onreadystatechange = null
        resolve()
      }
      document.head.appendChild(script)
    }),

    jq: (fn) => {
      if (typeof jQuery === 'undefined') {
        utils.js(deps.jquery).then(fn)
      } else {
        fn()
      }
    },
    
    onLoading: (el) => {
      if (el) {
        $(el).append('<div class="loading-wrap"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg></div>');
      }
    },
    onLoadSuccess: (el) => {
      if (el) {
        $(el).find('.loading-wrap').remove();
      }
    },
    onLoadFailure: (el) => {
      if (el) {
        $(el).find('.loading-wrap svg').remove();
        $(el).find('.loading-wrap').append('<svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" d="M12 3L21 20H3L12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.5s" values="60;0"/></path><path stroke-dasharray="6" stroke-dashoffset="6" d="M12 10V14"><animate fill="freeze" attributeName="stroke-dashoffset" begin="0.6s" dur="0.2s" values="6;0"/></path></g><circle cx="12" cy="17" r="1" fill="currentColor" fill-opacity="0"><animate fill="freeze" attributeName="fill-opacity" begin="0.8s" dur="0.4s" values="0;1"/></circle></svg>');
        $(el).find('.loading-wrap').addClass('error');
      }
    },
    request: (el, url, callback, onFailure) => {
      let retryTimes = 3;
      utils.onLoading(el);
      function req() {
        return new Promise((resolve, reject) => {
          let status = 0; // 0 等待 1 完成 2 超时
          let timer = setTimeout(() => {
            if (status === 0) {
              status = 2;
              timer = null;
              reject('请求超时');
              if (retryTimes == 0) {
                onFailure();
              }
            }
          }, 5000);
          fetch(url).then(function(response) {
            if (status !== 2) {
              clearTimeout(timer);
              resolve(response);
              timer = null;
              status = 1;
            }
            if (response.ok) {
              return response.json();
            }
            throw new Error('Network response was not ok.');
          }).then(function(data) {
            retryTimes = 0;
            utils.onLoadSuccess(el);
            callback(data);
          }).catch(function(error) {
            if (retryTimes > 0) {
              retryTimes -= 1;
              setTimeout(() => {
                req();
              }, 5000);
            } else {
              utils.onLoadFailure(el);
              onFailure();
            }
          });
        });
      }
      req();
    },
  };
</script>

<script>
  const sidebar = {
    leftbar: () => {
      if (l_body) {
        l_body.toggleAttribute('leftbar');
        l_body.removeAttribute('rightbar');
      }
    },
    rightbar: () => {
      if (l_body) {
        l_body.toggleAttribute('rightbar');
        l_body.removeAttribute('leftbar');
      }
    },
    dismiss: () => {
      if (l_body) {
        l_body.removeAttribute('leftbar');
        l_body.removeAttribute('rightbar');
      }
    },
    toggleTOC: () => {
      document.querySelector('#data-toc').classList.toggle('collapse');
    }
  }
</script>
<script type="text/javascript">
  (() => {
    const tagSwitchers = document.querySelectorAll('.tag-subtree.parent-tag > a > .tag-switcher-wrapper')
    for (const tagSwitcher of tagSwitchers) {
      tagSwitcher.addEventListener('click', (e) => {
        const parent = e.target.closest('.tag-subtree.parent-tag')
        parent.classList.toggle('expanded')
        e.preventDefault()
      })
    }

    // Get active tag from query string, then activate it.
    const urlParams = new URLSearchParams(window.location.search)
    const activeTag = urlParams.get('tag')
    if (activeTag) {
      let tag = document.querySelector(`.tag-subtree[data-tag="${activeTag}"]`)
      if (tag) {
        tag.querySelector('a').classList.add('active')
        
        while (tag) {
          tag.classList.add('expanded')
          tag = tag.parentElement.closest('.tag-subtree.parent-tag')
        }
      }
    }
  })()
</script>


<!-- required -->
<script src="/js/main.js?v=1.29.1" defer></script>

<script type="text/javascript">
  const applyTheme = (theme) => {
    if (theme === 'auto') {
      document.documentElement.removeAttribute('data-theme')
    } else {
      document.documentElement.setAttribute('data-theme', theme)
    }

    applyThemeToGiscus(theme)
  }

  const applyThemeToGiscus = (theme) => {
    theme = theme === 'auto' ? 'preferred_color_scheme' : theme

    const cmt = document.getElementById('giscus')
    if (cmt) {
      // This works before giscus load.
      cmt.setAttribute('data-theme', theme)
    }

    const iframe = document.querySelector('#comments > section.giscus > iframe')
    if (iframe) {
      // This works after giscus loaded.
      const src = iframe.src
      const newSrc = src.replace(/theme=[\w]+/, `theme=${theme}`)
      iframe.src = newSrc
    }
  }

  const switchTheme = () => {
    // light -> dark -> auto -> light -> ...
    const currentTheme = document.documentElement.getAttribute('data-theme')
    let newTheme;
    switch (currentTheme) {
      case 'light':
        newTheme = 'dark'
        break
      case 'dark':
        newTheme = 'auto'
        break
      default:
        newTheme = 'light'
    }
    applyTheme(newTheme)
    window.localStorage.setItem('Stellar.theme', newTheme)

    const messages = {
      light: `切换到浅色模式`,
      dark: `切换到深色模式`,
      auto: `切换到跟随系统配色`,
    }
    hud?.toast?.(messages[newTheme])
  }

  (() => {
    // Apply user's preferred theme, if any.
    const theme = window.localStorage.getItem('Stellar.theme')
    if (theme !== null) {
      applyTheme(theme)
    }
  })()
</script>


<!-- optional -->



<script defer>
  window.addEventListener('DOMContentLoaded', (event) => {
    ctx.services = Object.assign({}, JSON.parse(`{"mdrender":{"js":"/js/services/mdrender.js"},"siteinfo":{"js":"/js/services/siteinfo.js","api":null},"ghinfo":{"js":"/js/services/ghinfo.js"},"sites":{"js":"/js/services/sites.js"},"friends":{"js":"/js/services/friends.js"},"timeline":{"js":"/js/services/timeline.js"},"fcircle":{"js":"/js/services/fcircle.js"},"weibo":{"js":"/js/services/weibo.js"},"memos":{"js":"/js/services/memos.js"}}`));
    for (let id of Object.keys(ctx.services)) {
      const js = ctx.services[id].js;
      if (id == 'siteinfo') {
        ctx.cardlinks = document.querySelectorAll('a.link-card[cardlink]');
        if (ctx.cardlinks?.length > 0) {
          utils.js(js, { defer: true }).then(function () {
            setCardLink(ctx.cardlinks);
          });
        }
      } else {
        const els = document.getElementsByClassName(`ds-${id}`);
        if (els?.length > 0) {
          utils.jq(() => {
            if (id == 'timeline' || 'memos' || 'marked') {
              utils.js(deps.marked).then(function () {
                utils.js(js, { defer: true });
              });
            } else {
              utils.js(js, { defer: true });
            }
          });
        }
      }
    }
  });
</script>

<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    ctx.search = {
      path: `/search.json`,
    }
    utils.js('/js/search/local-search.js', { defer: true });
  });
</script><script>
  window.FPConfig = {
    delay: 0,
    ignoreKeywords: [],
    maxRPS: 5,
    hoverDelay: 25
  };
</script>
<script defer src="https://cdn.jsdelivr.net/npm/flying-pages@2/flying-pages.min.js"></script><script defer src="https://cdn.jsdelivr.net/npm/vanilla-lazyload@19.1.3/dist/lazyload.min.js"></script>
<script>
  // https://www.npmjs.com/package/vanilla-lazyload
  // Set the options globally
  // to make LazyLoad self-initialize
  window.lazyLoadOptions = {
    elements_selector: ".lazy",
  };
  // Listen to the initialization event
  // and get the instance of LazyLoad
  window.addEventListener(
    "LazyLoad::Initialized",
    function (event) {
      window.lazyLoadInstance = event.detail.instance;
    },
    false
  );
  document.addEventListener('DOMContentLoaded', function () {
    window.lazyLoadInstance?.update();
  });
</script><script>
  ctx.fancybox = {
    selector: `.timenode p>img`,
    css: `https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css`,
    js: `https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js`
  };
  var selector = '[data-fancybox]:not(.error)';
  if (ctx.fancybox.selector) {
    selector += `, ${ctx.fancybox.selector}`
  }
  var needFancybox = document.querySelectorAll(selector).length !== 0;
  if (!needFancybox) {
    const els = document.getElementsByClassName('ds-memos');
    if (els != undefined && els.length > 0) {
      needFancybox = true;
    }
  }
  if (needFancybox) {
    utils.css(ctx.fancybox.css);
    utils.js(ctx.fancybox.js, { defer: true }).then(function () {
      Fancybox.bind(selector, {
        hideScrollbar: false,
        Thumbs: {
          autoStart: false,
        },
        caption: (fancybox, slide) => {
          return slide.triggerEl.alt || slide.triggerEl.dataset.caption || null
        }
      });
    })
  }
</script>
<script>
  window.addEventListener('DOMContentLoaded', (event) => {
    const swiper_api = document.getElementById('swiper-api');
    if (swiper_api != undefined) {
      utils.css(`https://unpkg.com/swiper@10.3.1/swiper-bundle.min.css`);
      utils.js(`https://unpkg.com/swiper@10.3.1/swiper-bundle.min.js`, { defer: true }).then(function () {
        const effect = swiper_api.getAttribute('effect') || '';
        var swiper = new Swiper('.swiper#swiper-api', {
          slidesPerView: 'auto',
          spaceBetween: 8,
          centeredSlides: true,
          effect: effect,
          rewind: true,
          pagination: {
            el: '.swiper-pagination',
            clickable: true,
          },
          navigation: {
            nextEl: '.swiper-button-next',
            prevEl: '.swiper-button-prev',
          },
        });
      })
    }
  });
</script>
<script>
  document.addEventListener('DOMContentLoaded', function () {
    window.codeElements = document.querySelectorAll('.code');
    if (window.codeElements.length > 0) {
      ctx.copycode = {
        default_text: `Copy`,
        success_text: `Copied`,
        toast: `复制成功`,
      };
      utils.js('/js/plugins/copycode.js');
    }
  });
</script>


<!-- inject -->

</div></body></html>
